package jsr292.mock;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import jsr292.weaver.Utils;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.Remapper;
import org.objectweb.asm.commons.RemappingClassAdapter;

public class Diff {
    
    static class MemberMirror {
	private final int access;
	private final String nameAndDesc;

	public MemberMirror(int access, String nameAndDesc) {
	    this.access = access;
	    this.nameAndDesc = nameAndDesc;
	}

	public int getAccess() {
            return access;
        }
	public String getNameAndDesc() {
            return nameAndDesc;
        }
	
	@Override
	public String toString() {
	    return Modifier.toString(access)+' '+nameAndDesc;
	}
    }
    
    static class ClassMirror {
        private final String name;
	final LinkedHashMap<String, MemberMirror> members =
	    new LinkedHashMap<String, MemberMirror>();
	
	public ClassMirror(String name) {
	    this.name = name;
	}

	public String getName() {
	    return name;
	}
	
	public void addMember(int access, String nameAndDesc) {
	    members.put(nameAndDesc, new MemberMirror(access, nameAndDesc));
	}
	
	@Override
	public String toString() {
	    return name;
	}
    }
    
    private static void diff(Map<String, ClassMirror> classMap1, Map<String, ClassMirror> classMap2) {
	for(ClassMirror classMirror1:classMap1.values()) {
	    ClassMirror classMirror2 = classMap2.get(classMirror1.getName());
	    if (classMirror2 == null) {
		System.err.println("no class equivalent to "+classMirror1);
		continue;
	    }
	    
	    diff(classMirror1, classMirror2);
	}
    }
    
    private static void diff(ClassMirror class1, ClassMirror class2) {
	for(MemberMirror member:class1.members.values()) {
	    MemberMirror member2 = class2.members.get(member.getNameAndDesc());
            if (member2 == null) {
		System.err.println("member "+member+" of class "+class1+ " has no equivalent member");
		continue;
	    }
            
            if ((member.getAccess() & 15) != (member2.getAccess() & 15)) {
                System.err.println("member "+member+" of class "+class1+ " has not same access modifiers");
            }
	}
    }
    
    static class Filter {
	public ClassVisitor filter(ClassVisitor cv) {
	    return cv;
	}
    }
    
    private static final Filter IDENTITY_FILTER = new Filter();
    private static final Filter RENAME_FILTER= new Filter() {
	@Override
	public ClassVisitor filter(ClassVisitor cv) {
	    return new RemappingClassAdapter(cv, new Remapper() {
	        @Override
	        public String map(String typeName) {
		    if (typeName.startsWith(Utils.JAVA_LANG_INVOKE+'/')) {
			return Utils.JSR292_PACKAGE_PREFIX+typeName;
		    }
		    if (typeName.equals(Utils.JAVA_LANG_CLASS_VALUE)) {
		        return Utils.JSR292_CLASS_VALUE;
		    }
		    if (typeName.equals(Utils.JAVA_LANG_UTIL_OBJECTS)) {
                        return Utils.JSR292_OBJECTS;
                    }
		    return typeName;
		}
	    });
	}  
    };
    
    @SuppressWarnings("serial")
    private static class SkipException extends RuntimeException { 
	SkipException() {
	    // enforce singleton
	}
    }
    final static SkipException SKIP = new SkipException();
    
    private static ClassMirror mirrorify(ClassReader reader, Filter filter) {
	final ClassMirror[] cms = new ClassMirror[1];
	try {
	    reader.accept(filter.filter(new ClassVisitor(Opcodes.ASM4) {
		ClassMirror cm;
		@Override
		public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
		    if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0) {
			throw SKIP;
		    }
		    cms[0] = cm = new ClassMirror(name);
		}
		@Override
		public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
		    if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0) {
			return null;
		    }

		    cm.addMember(access, name+';'+desc);
		    return null;
		}
		@Override
		public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
		    if ((access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0) {
			return null;
		    }

		    cm.addMember(access, name + desc);
		    return null;
		}

	    }), ClassReader.SKIP_CODE);
	} catch(SkipException e) {
	    return null;
	}
	return cms[0];
    }
    
    private static LinkedHashMap<String, ClassMirror> mirrorify(JarFile input, Filter filter) throws IOException {
	LinkedHashMap<String, ClassMirror> map = new LinkedHashMap<String, ClassMirror>();
	
	for(JarEntry entry : Collections.list(input.entries())) {
	    String name = entry.getName();
	    if (!name.endsWith(".class")) {
		System.err.println("skipping "+name);
	        continue;
	    }

	    InputStream inputStream = input.getInputStream(entry);
	    try {
	        ClassReader reader = new ClassReader(inputStream);
	        ClassMirror classMirror = mirrorify(reader, filter);
	        if (classMirror == null)
	            continue;

	        map.put(classMirror.getName(), classMirror);
	    } finally {
	        inputStream.close();
	    }
	}
	input.close();
	
	return map;
    }
    
    public static void main(String[] args) throws IOException {
        JarFile jdk7Jar = new JarFile(args[0]);
	JarFile backportJar = new JarFile(args[1]);
	
	LinkedHashMap<String, ClassMirror> jdk7ClassMap = mirrorify(jdk7Jar, RENAME_FILTER);
	LinkedHashMap<String, ClassMirror> backportClassMap = mirrorify(backportJar, IDENTITY_FILTER);
	
	diff(jdk7ClassMap, backportClassMap);
    }
}
